/**
 * html5 file upload
 *
 * support multiple file upload
 */
type Options = {
    /**
     * HTMLInputElement
     */
    fileElement: HTMLInputElement | null
    /**
     * Upload post params
     */
    postParams: Record<string, any> | null
    /**
     * Upload headers
     */
    headers: Record<string, any> | null
    /**
     * Upload url
     */
    server: string
    /**
     * Upload file name
     */
    fileName: string
    /**
     * Use formdata to upload file
     */
    useFormData: boolean
    withCredentials: boolean

    /**
     * Auto upload
     */
    auto: boolean
    /**
     * Support multiple files
     */
    multiple: boolean
    /**
     * The file types that can select
     */
    accept: string
    /**
     * The max size of upload file in bytes
     */
    fileSizeLimit: number

    [key: string]: any
}

export default class FileUploader {
    static ERROR_FILE_SIZE_LIMIT = 1;
    static ERROR_INVALID_FILE_TYPE = 2;

    xhr: XMLHttpRequest | null = null;
    doc: Document = document;
    filesQueue: Queue<UploadFile> | null = null;
    fileQueuedHandler: ((file: UploadFile) => void) | null = null;
    fileQueuedErrorHandler: ((file: UploadFile, errorType: number) => void) | null = null;
    filesQueuedCompleteHandler: ((data: { selected: number, queued: number, files: UploadFile[] }) => void) | null = null;
    uploadProgressHandler: ((file: UploadFile, percent: number) => void) | null = null;
    uploadSuccessHandler: ((file: UploadFile, data: string) => void) | null = null;
    uploadCompleteHandler: (() => void) | null = null;

    isUploading = false;
    configs: Options = {
        fileElement: null,
        postParams: null,
        headers: null,
        server: '',
        fileName: 'file',
        useFormData: true,
        withCredentials: false,

        auto: false,
        multiple: false,
        accept: 'image/jpg, image/jpeg, image/png, image/gif',
        // 5 Mb
        fileSizeLimit: 1024 * 1024 * 5
    };

    constructor(options: Partial<Options>) {
        this.initOptions(options);
        this.initFileComponent();
    }

    setupXHR(file: UploadFile): void {
        this.xhr = new XMLHttpRequest();
        this.xhr.ontimeout = this.xhr.onabort = this.xhr.onerror = (e) => {
            this.isUploading = false;
        };

        this.xhr.upload.onprogress = (e) => {
            let percent = 0;
            if (e.lengthComputable) {
                percent = e.loaded / e.total;
            }

            if (null !== this.uploadProgressHandler) {
                this.uploadProgressHandler(file, percent);
            }
        };

        this.xhr.onreadystatechange = () => {
            if (4 !== this.xhr!.readyState) {
                return;
            }

            this.xhr!.upload.onprogress = null;
            this.xhr!.onreadystatechange = null;

            if (200 === this.xhr!.status) {
                if (null !== this.uploadSuccessHandler) {
                    this.uploadSuccessHandler(file, this.xhr!.responseText)
                }
            }

            this.isUploading = false;

            // Next
            this.startUpload();
        };

        this.xhr.open('POST', this.configs.server, true);
        if (this.configs.withCredentials) {
            this.xhr.withCredentials = true;
        }

        // headers
        if(null !== this.configs.headers) {
            for (let k in this.configs.headers) {
                this.xhr.setRequestHeader(k, this.configs.headers[k]);
            }
        }
    }

    clearFileInput(): void {
        if (this.configs.fileElement) {
            this.configs.fileElement.value = '';
        }
    }

    initOptions(options: Partial<Options>): void {
        for(let k in options) {
            this.configs[k] = options[k];
        }

        // 自动上传
        if (this.configs.auto) {
            this.filesQueuedCompleteHandler = () => {
                this.startUpload();
            };
        }
    }

    initFileComponent(): void {
        if (null === this.configs.fileElement) {
            return;
        }

        this.configs.fileElement.setAttribute('accept', this.configs.accept);
        if (this.configs.multiple) {
            this.configs.fileElement.setAttribute('multiple', 'multiple');
        }

        this.configs.fileElement.onchange = (e) => {
            this.selectFiles((e.target as any).files);
        };
    }

    isValidFile(file: UploadFile): { valid: boolean, type: number, message: string } {
        if (file.size > this.configs.fileSizeLimit) {
            return {
                valid: false,
                type: FileUploader.ERROR_FILE_SIZE_LIMIT,
                message: 'File is too big'
            };
        }

        if (-1 === this.configs.accept.indexOf(file.extension)) {
            return {
                valid: false,
                type: FileUploader.ERROR_INVALID_FILE_TYPE,
                message: 'File type is invalid'
            };
        }

        return {
            valid: true,
            type: 0,
            message: ''
        }
    }

    /**
     * Add files to queue
     */
    selectFiles(fileList: File[]): void {
        if (fileList.length <= 0) {
            return;
        }

        if (null === this.filesQueue) {
            this.filesQueue = new Queue();
        }

        let len = fileList.length;
        let tmpFile: UploadFile;
        for (let i = 0; i < len; i++) {
            tmpFile = new UploadFile(fileList[i]);

            // Check file
            let validate = this.isValidFile(tmpFile);
            if (!validate.valid) {
                if (null !== this.fileQueuedErrorHandler) {
                    this.fileQueuedErrorHandler(tmpFile, validate.type)
                }
                continue;
            }

            this.filesQueue.add(tmpFile);

            if (null !== this.fileQueuedHandler) {
                this.fileQueuedHandler(tmpFile)
            }
        }

        if (null !== this.filesQueuedCompleteHandler) {
            this.filesQueuedCompleteHandler({
                selected: len,
                queued: this.filesQueue.size,
                files: this.filesQueue.toArray()
            })
        }
    }

    uploadAsFormData(file: UploadFile): void {
        let fd = new FormData();
        if (null !== this.configs.postParams) {
            for (let k in this.configs.postParams) {
                fd.append(k, this.configs.postParams[k]);
            }
        }
        fd.append(this.configs.fileName, file.nativeFile);
        this.setupXHR(file);
        this.xhr!.send(fd);
    }

    uploadAsBinary(file: UploadFile): void {
        let fr = new FileReader();
        fr.onload = () => {
            this.xhr!.send(fr.result);
            fr.onload = null;
            fr = null as any;
        };

        this.setupXHR(file);
        this.xhr!.overrideMimeType('application/octet-stream');
        fr.readAsArrayBuffer(file.nativeFile);
    }

    /**
     * Set post parameters
     */
    setPostParams(paramsObject: Record<string, any>): void {
        this.configs.postParams = paramsObject;
    }

    /**
     * Append or replace a post parameter
     */
    addPostParam(name: string, value: any): void {
        if(null === this.configs.postParams) {
            this.configs.postParams = {};
        }
        this.configs.postParams[name] = value;
    }

    /**
     * Start upload
     */
    startUpload(): void {
        if (this.isUploading) {
            return;
        }
        if (null === this.filesQueue) {
            return;
        }

        let file = this.filesQueue.take();
        if (null === file) {
            this.clearFileInput();
            if (null !== this.uploadCompleteHandler) {
                this.uploadCompleteHandler()
            }
            return;
        }

        this.isUploading = true;
        if (this.configs.useFormData) {
            this.uploadAsFormData(file);
            return;
        }
        this.uploadAsBinary(file);
    }
}

export class Queue<T> {
    headNode: QueueNode<T> | null;
    tailNode: QueueNode<T> | null;
    size: number;

    constructor() {
        this.headNode = null;
        this.tailNode = null;
        this.size = 0;
    }

    add(data: T): void {
        let node = new QueueNode<T>(data, null);
        this.tailNode = node;

        if (0 === this.size) {
            this.headNode = node;
        } else {
            this.tailNode.next = node;
        }

        this.size++;
    }

    take(): T | null {
        if (0 === this.size || null === this.headNode) {
            return null;
        }

        let data = this.headNode.data;
        let tmpHeadNode = this.headNode;

        // 从队列去除头节点
        this.headNode = tmpHeadNode.next;
        tmpHeadNode.next = null;
        tmpHeadNode = null as any;

        // 没节点了
        if (null === this.headNode) {
            this.headNode = this.tailNode = null;
        }
        this.size--;

        return data;
    }

    delete(data: T): void {
        let prev: QueueNode<T> | null = null;
        let current: QueueNode<T> | null = null;
        for (current = this.headNode; null !== current; prev = current, current = current.next) {
            if (data === current.data) {
                // 前一个节点绕过本节点
                if (null !== prev) {
                    prev.next = current.next
                }

                // 删除头节点
                if (current === this.headNode) {
                    this.headNode = current.next;
                }
                // 删除尾节点
                if (current === this.tailNode) {
                    this.tailNode = prev;
                }

                current.next = null;

                this.size -= 1;
                break;
            }
        }
    }

    clear(): void {
        while (null !== this.take()) { }
    }

    toArray(): T[] {
        let ret: T[] = [];

        let i = 0;
        let current: QueueNode<T> | null = null;
        for (current = this.headNode; null !== current; current = current.next) {
            ret.push(current.data);
            i++;
        }

        return ret;
    }
}

export class QueueNode<T> {
    data: T;
    next: QueueNode<T> | null;

    constructor(data: any, next: QueueNode<T> | null) {
        this.data = data;
        this.next = next;
    }
}

export class UploadFile {
    static uuid = 0;

    nativeFile: File
    id: string
    name: string
    size: number
    type: string
    extension: string
    lastModified: number

    constructor(file: File) {
        this.nativeFile = file;
        this.id = 'xef' + UploadFile.uuid++;
        this.name = file.name;
        this.size = file.size;
        this.type = file.type;
        this.extension = file.type.substring(file.type.indexOf('/') + 1);
        this.lastModified = file.lastModified;
    }
}
